#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Based on GAppProxy by Du XiaoGang dugang@188.com
# Some idea from GAppProxy2 by fcicq@fcicq.net
# thx them

__author__ = 'd3d3LmVodXN0QGdtYWlsLmNvbQ=='.decode('base64')
__version__ = '0.4.0'

import zlib, logging, time, re, struct
from google.appengine.ext import webapp
from google.appengine.ext import db
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.api import urlfetch, memcache
from google.appengine.runtime import apiproxy_errors
import Wcrypto

def _unquote_data(s):
    unquote_map = {'0':'\x10', '1':'=', '2':'&'}
    res = s.split('\x10')
    for i in xrange(1, len(res)):
        item = res[i]
        try:
            res[i] = unquote_map[item[0]] + item[1:]
        except KeyError:
            res[i] = '\x10' + item
    return ''.join(res)

def decode_data(qs, keep_blank_values=False, strict_parsing=False):
    pairs = qs.split('&')
    dic = {}
    for name_value in pairs:
        if not name_value and not strict_parsing:
            continue
        nv = name_value.split('=', 1)
        if len(nv) != 2:
            if strict_parsing:
                raise ValueError, "bad query field: %r" % (name_value,)
            if keep_blank_values:
                nv.append('')
            else:
                continue
        if len(nv[1]) or keep_blank_values:
            dic[_unquote_data(nv[0])] = _unquote_data(nv[1])
    return dic

def _quote_data(s):
    return str(s).replace('\x10', '\x100').replace('=','\x101').replace('&','\x102')

def encode_data(dic):
    res = []
    for k,v in dic.iteritems():
        res.append('%s=%s' % (_quote_data(k), _quote_data(v)))
    return '&'.join(res)

def encrypt(data):
    return cfg['crytoobj'].encrypt(data, cfg['siteKey'])

def decrypt(data):
    return cfg['crytoobj'].decrypt(data, cfg['siteKey'])

class WPConfig(db.Model):
    cfgCacheTime = db.IntegerProperty(required=True)
    cacheTime = db.IntegerProperty(required=True)
    rangeSize = db.IntegerProperty(required=True)
    siteKey = db.StringProperty()
    cryptomode = db.StringProperty()

def getConfig():
    config = memcache.get('config', namespace='wp_config')
    if config is None:
        cfg = WPConfig.all().get()
        if not cfg:#No Entry
            cfg = WPConfig(cfgCacheTime=5*60, cacheTime=24*3600, rangeSize=512*1000,
                siteKey=u'', cryptomode=u'AES-CBC-32')
            cfg.put()
        config = {'cacheTime':cfg.cacheTime, 'rangeSize':cfg.rangeSize,
                  'siteKey':cfg.siteKey.encode('utf-8').decode('string_escape'),
                  'cryptomode':cfg.cryptomode.encode('utf-8')}
        if not memcache.set('config', config, cfg.cfgCacheTime, namespace='wp_config'):
            logging.error('Memcache set wp_config failed')
    config['crytoobj'] = Wcrypto.Wcrypto(config['cryptomode'])
    return config

class MainHandler(webapp.RequestHandler):
    #FRS_Headers = ('', 'content-length', 'keep-alive', 'host', 'x-forwarded-for',
    #               'vary', 'via', 'proxy-authorization', 'proxy-connection', 'upgrade')
    FRP_Headers = ('', 'x-google-cache-control', 'via')
    Fetch_Max = 2
    Deadline = (5, 10)

    def sendResponse(self, status_code, headers, content='', method='', url=''):
        self.response.headers['Content-Type'] = 'application/octet-stream'
        # Cache hits
        if headers is None:
            logging.debug('Memcache hits: "%s %s" %d %d' % (method, url, status_code, len(content)))
            return self.response.out.write(encrypt(content))
        contentType = headers.get('content-type', '').lower()
        # Check cache_need
        use_cache = False
        if method in ('HEAD', 'GET') and status_code==200 and contentType.find('text/html')<0 \
            and cfg['cacheTime'] and headers.get('pragma', '').find('no-cache')<0 \
            and 'set-cookie' not in headers and headers.get('cache-control', '').find('no-cache')<0:
            t = time.gmtime(time.time() + cfg['cacheTime'])
            headers['x-cache'] = 'HIT from WallProxy'
            headers['expires'] = time.strftime('%a, %d %b %Y %H:%M:%S GMT', t)
            headers['cache-control'] = 'public, max-age=%d' % cfg['cacheTime']
            headers['last-modified'] = time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime())
            use_cache = True
        headers = encode_data(headers)
        # Build send-data
        rdata = '%s%s%s' % (struct.pack('>3I', status_code, len(headers), len(content)), headers, content)
        if contentType.find('text')>=0 or contentType.find('application')>=0:
            data = zlib.compress(rdata)
            data = '1'+data if len(rdata)>len(data) else '0'+rdata
        else:
            data = '0' + rdata
        # Cache data
        if use_cache and len(data)<1000000:
            if not memcache.set(url, data, cfg['cacheTime'], namespace='wp_cache'):
                logging.warning('Memcache set wp_cache(%s) failed' % url)
        if status_code == 555:
            logging.warning('Response: "%s %s" %s' % (method, url, content))
        else:
            logging.debug('Response: "%s %s" %d %d/%d/%d' % (method, url, status_code,
                len(content), len(rdata), len(data)))
        return self.response.out.write(encrypt(data))

    def sendNotify(self, status_code, content, method='', url='', fullContent=False):
        if not fullContent and status_code!=555:
            content = '<h2>Fetch Server Info</h2><hr noshade="noshade"><p>Code: %d</p>' \
                      '<p>Message: %s</p>' % (status_code, content)
        headers = {'server':'WallProxy/0.3', 'content-type':'text/html', 'content-length':len(content)}
        self.sendResponse(status_code, headers, content, method, url)

    def post(self):
        global cfg
        try:
            request = decrypt(self.request.body)
            request = decode_data(zlib.decompress(request))
        except:
            return self.response.out.write('Hello World!')

        method = request.get('method', 'GET')
        if not hasattr(urlfetch, method):
            return self.sendNotify(555, 'Invalid Method', method)
        fetch_method = getattr(urlfetch, method)

        url = request.get('url', '')
        if not url.startswith('http'):
            return self.sendNotify(555, 'Unsupported Scheme', method, url)
        if url.startswith('http://wallproxy/'):
            import os
            appid = '%s v%s' % (os.environ['APPLICATION_ID'], os.environ['CURRENT_VERSION_ID'])
            path = url[len('http://wallproxy'):].lower()
            if path == '/cache':
                return self.sendNotify(200, '<h2>Memcache Status(%s)</h2><hr noshade="noshade">' \
                    '<p>%s</p>' % (appid, memcache.get_stats()), method, url, True)
            elif path == '/cache/reset' and memcache.flush_all():
                cfg = getConfig() #reset password
                return self.sendNotify(200, 'Memcache Reseted(%s)' % appid, method, url)
            else:
                resp = []
                for k,v in cfg.iteritems():
                    resp.append('%s: %s' % (k.upper(), v))
                resp = '<h2>WallProxy Configuration(%s)</h2><hr noshade="noshade">' \
                       '<pre>%s</pre>' % (appid, '\n'.join(resp))
                return self.sendNotify(200, resp, method, url, True)

        payload = request.get('payload', '')
        deadline = MainHandler.Deadline[1 if payload else 0]
        cache_content = None
        if cfg['cacheTime'] and method in ('HEAD', 'GET'):
            cache_content = memcache.get(url, namespace='wp_cache')

        fetch_range = 'bytes=0-%d' % (cfg['rangeSize'] - 1)
        rangeFetch = False
        headers = {}
        for line in request.get('headers', '').split('\n'):
            if ':' not in line:
                continue
            key, value = map(str.strip, line.split(':', 1))
            keyl = key.lower()
            #if keyl in MainHandler.FRS_Headers:
            #    continue
            if keyl == 'rangefetch':
                rangeFetch = True
                continue
            if keyl=='if-modified-since' and cache_content:
                return self.sendResponse(304, {'x-cache':'HIT from WallProxy'}, '', method, url)
            if keyl=='range' and not rangeFetch:
                m = re.search(r'(\d+)?-(\d+)?', value)
                if not m: continue
                m = [u and int(u) for u in m.groups()]
                if m[0] is None and m[1] is None: continue
                if m[0] is None:
                    if m[1] > cfg['rangeSize']: m[1] = 1023
                elif m[1] is None or m[1]-m[0]+1 > cfg['rangeSize']:
                    m[1] = cfg['rangeSize'] - 1 + m[0]
                fetch_range = ('bytes=%s-%s' % (m[0], m[1])).replace('None', '')
            headers[key] = value
        headers['Connection'] = 'close'
        if cache_content:
            return self.sendResponse(200, None, cache_content, method, url)

        for i in range(MainHandler.Fetch_Max):
            try:
                response = urlfetch.fetch(url, payload, fetch_method, headers, False, False, deadline)
                #if method=='GET' and len(response.content)>0x1000000:
                #    raise urlfetch.ResponseTooLargeError(None)
                break
            except apiproxy_errors.OverQuotaError, e:
                time.sleep(4)
            except urlfetch.InvalidURLError, e:
                return self.sendNotify(555, 'Invalid URL: %s' % e, method, url)
            except urlfetch.ResponseTooLargeError, e:
                if method == 'GET':
                    deadline = MainHandler.Deadline[1]
                    if not rangeFetch: headers['Range'] = fetch_range
                else:
                    return self.sendNotify(555, 'Response Too Large: %s' % e, method, url)
            except Exception, e:
                if i==0 and method=='GET':
                    deadline = MainHandler.Deadline[1]
                    if not rangeFetch: headers['Range'] = fetch_range
        else:
            return self.sendNotify(555, 'Urlfetch error: %s' % e, method, url)

        for k in MainHandler.FRP_Headers:
            if k in response.headers:
                del response.headers[k]
        if 'set-cookie' in response.headers:
            scs = response.headers['set-cookie'].split(', ')
            cookies = []
            i = -1
            for sc in scs:
                if re.match(r'[^ =]+ ', sc):
                    try:
                        cookies[i] += ', '+sc
                    except:
                        pass
                else:
                    cookies.append(sc)
                    i += 1
            response.headers['set-cookie'] = '\r\nSet-Cookie: '.join(cookies)
        response.headers['connection'] = 'close'
        return self.sendResponse(response.status_code, response.headers, response.content, method, url)

    def get(self):
        self.redirect('http://twitter.com/hexieshe')

def main():
    application = webapp.WSGIApplication([(r'/.*', MainHandler)], debug=True)
    run_wsgi_app(application)

if __name__ == '__main__':
    cfg = getConfig()
    main()